﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace WpWinNl.Utilities
{
  /// <summary>
  /// SuspensionManager captures global session state to simplify process lifetime management
  /// for an application.  Note that session state will be automatically cleared under a variety
  /// of conditions and should only be used to store information that would be convenient to
  /// carry across sessions, but that should be discarded when an application crashes or is
  /// upgraded.
  /// </summary>
  internal sealed class SuspensionHelper
  {
    private static Dictionary<string, object> _sessionState = new Dictionary<string, object>();
    private static List<Type> _knownTypes = new List<Type>();
    private const string sessionStateFilename = "_sessionState.xml";

    /// <summary>
    /// Provides access to global session state for the current session.  This state is
    /// serialized by <see cref="SaveAsync"/> and restored by
    /// <see cref="RestoreAsync"/>, so values must be serializable by
    /// <see cref="DataContractSerializer"/> and should be as compact as possible.  Strings
    /// and other self-contained data types are strongly recommended.
    /// </summary>
    public static Dictionary<string, object> SessionState
    {
      get { return _sessionState; }
    }

    /// <summary>
    /// List of custom types provided to the <see cref="DataContractSerializer"/> when
    /// reading and writing session state.  Initially empty, additional types may be
    /// added to customize the serialization process.
    /// </summary>
    public static List<Type> KnownTypes
    {
      get { return _knownTypes; }
    }

    /// <summary>
    /// Save the current <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
    /// registered with <see cref="RegisterFrame"/> will also preserve their current
    /// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity
    /// to save its state.
    /// </summary>
    /// <returns>An asynchronous task that reflects when session state has been saved.</returns>
    public static async Task SaveAsync()
    {
      try
      {
        // Save the navigation state for all registered frames
        foreach (var weakFrameReference in _registeredFrames)
        {
          Frame frame;
          if (weakFrameReference.TryGetTarget(out frame))
          {
            SaveFrameNavigationState(frame);
          }
        }

        // Serialize the session state synchronously to avoid asynchronous access to shared
        // state
        MemoryStream sessionData = new MemoryStream();
        DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), _knownTypes);
        serializer.WriteObject(sessionData, _sessionState);

        // Get an output stream for the SessionState file and write the state asynchronously
        StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(sessionStateFilename, CreationCollisionOption.ReplaceExisting);
        using (Stream fileStream = await file.OpenStreamForWriteAsync())
        {
          sessionData.Seek(0, SeekOrigin.Begin);
          await sessionData.CopyToAsync(fileStream);
        }
      }
      catch (Exception e)
      {
        throw new SuspensionManagerException(e);
      }
    }

    /// <summary>
    /// Restores previously saved <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
    /// registered with <see cref="RegisterFrame"/> will also restore their prior navigation
    /// state, which in turn gives their active <see cref="Page"/> an opportunity restore its
    /// state.
    /// </summary>
    /// <returns>An asynchronous task that reflects when session state has been read.  The
    /// content of <see cref="SessionState"/> should not be relied upon until this task
    /// completes.</returns>
    public static async Task RestoreAsync()
    {
      _sessionState = new Dictionary<String, Object>();

      try
      {
        // Get the input stream for the SessionState file
        StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(sessionStateFilename);
        using (IInputStream inStream = await file.OpenSequentialReadAsync())
        {
          // Deserialize the Session State
          DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), _knownTypes);
          _sessionState = (Dictionary<string, object>)serializer.ReadObject(inStream.AsStreamForRead());
        }

        // Restore any registered frames to their saved state
        foreach (var weakFrameReference in _registeredFrames)
        {
          Frame frame;
          if (weakFrameReference.TryGetTarget(out frame))
          {
            frame.ClearValue(FrameSessionStateProperty);
            RestoreFrameNavigationState(frame);
          }
        }
      }
      catch (Exception e)
      {
        throw new SuspensionManagerException(e);
      }
    }

    private static DependencyProperty FrameSessionStateKeyProperty =
        DependencyProperty.RegisterAttached("_FrameSessionStateKey", typeof(String), typeof(SuspensionHelper), null);
    private static DependencyProperty FrameSessionStateProperty =
        DependencyProperty.RegisterAttached("_FrameSessionState", typeof(Dictionary<String, Object>), typeof(SuspensionHelper), null);
    private static List<WeakReference<Frame>> _registeredFrames = new List<WeakReference<Frame>>();

    /// <summary>
    /// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to
    /// and restored from <see cref="SessionState"/>.  Frames should be registered once
    /// immediately after creation if they will participate in session state management.  Upon
    /// registration if state has already been restored for the specified key
    /// the navigation history will immediately be restored.  Subsequent invocations of
    /// <see cref="RestoreAsync"/> will also restore navigation history.
    /// </summary>
    /// <param name="frame">An instance whose navigation history should be managed by
    /// <see cref="SuspensionHelper"/></param>
    /// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to
    /// store navigation-related information.</param>
    public static void RegisterFrame(Frame frame, String sessionStateKey)
    {
      if (frame.GetValue(FrameSessionStateKeyProperty) != null)
      {
        throw new InvalidOperationException("Frames can only be registered to one session state key");
      }

      if (frame.GetValue(FrameSessionStateProperty) != null)
      {
        throw new InvalidOperationException("Frames must be either be registered before accessing frame session state, or not registered at all");
      }

      // Use a dependency property to associate the session key with a frame, and keep a list of frames whose
      // navigation state should be managed
      frame.SetValue(FrameSessionStateKeyProperty, sessionStateKey);
      _registeredFrames.Add(new WeakReference<Frame>(frame));

      // Check to see if navigation state can be restored
      RestoreFrameNavigationState(frame);
    }

    /// <summary>
    /// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/>
    /// from <see cref="SessionState"/>.  Any navigation state previously captured will be
    /// removed.
    /// </summary>
    /// <param name="frame">An instance whose navigation history should no longer be
    /// managed.</param>
    public static void UnregisterFrame(Frame frame)
    {
      // Remove session state and remove the frame from the list of frames whose navigation
      // state will be saved (along with any weak references that are no longer reachable)
      SessionState.Remove((String)frame.GetValue(FrameSessionStateKeyProperty));
      _registeredFrames.RemoveAll((weakFrameReference) =>
      {
        Frame testFrame;
        return !weakFrameReference.TryGetTarget(out testFrame) || testFrame == frame;
      });
    }

    /// <summary>
    /// Provides storage for session state associated with the specified <see cref="Frame"/>.
    /// Frames that have been previously registered with <see cref="RegisterFrame"/> have
    /// their session state saved and restored automatically as a part of the global
    /// <see cref="SessionState"/>.  Frames that are not registered have transient state
    /// that can still be useful when restoring pages that have been discarded from the
    /// navigation cache.
    /// </summary>
    /// <remarks>Apps may choose to rely on <see cref="NavigationHelper"/> to manage
    /// page-specific state instead of working with frame session state directly.</remarks>
    /// <param name="frame">The instance for which session state is desired.</param>
    /// <returns>A collection of state subject to the same serialization mechanism as
    /// <see cref="SessionState"/>.</returns>
    public static Dictionary<String, Object> SessionStateForFrame(Frame frame)
    {
      var frameState = (Dictionary<String, Object>)frame.GetValue(FrameSessionStateProperty);

      if (frameState == null)
      {
        var frameSessionKey = (String)frame.GetValue(FrameSessionStateKeyProperty);
        if (frameSessionKey != null)
        {
          // Registered frames reflect the corresponding session state
          if (!_sessionState.ContainsKey(frameSessionKey))
          {
            _sessionState[frameSessionKey] = new Dictionary<String, Object>();
          }
          frameState = (Dictionary<String, Object>)_sessionState[frameSessionKey];
        }
        else
        {
          // Frames that aren't registered have transient state
          frameState = new Dictionary<String, Object>();
        }
        frame.SetValue(FrameSessionStateProperty, frameState);
      }
      return frameState;
    }

    private static void RestoreFrameNavigationState(Frame frame)
    {
      var frameState = SessionStateForFrame(frame);
      if (frameState.ContainsKey("Navigation"))
      {
        frame.SetNavigationState((String)frameState["Navigation"]);
      }
    }

    private static void SaveFrameNavigationState(Frame frame)
    {
      var frameState = SessionStateForFrame(frame);
      frameState["Navigation"] = frame.GetNavigationState();
    }
  }
  public class SuspensionManagerException : Exception
  {
    public SuspensionManagerException()
    {
    }

    public SuspensionManagerException(Exception e)
      : base("SuspensionManager failed", e)
    {

    }
  }
}
